<template>
  <div ref="container"
       class="xdh-grid__container"
       :class="classes">
    <div class="xdh-grid__item"
         v-for="(item,index) in originalList"
         :data-id="index"
         :key="index"
         :style="styles(item)">
      <slot :index="index" :item="item" :props="props">{{index}}</slot>
    </div>
  </div>

</template>

<script>
  /**
   * grid组件
   * @module widgets/xdh-grid
   * @example
   * // 使用说明
   */
  import Sortable from 'sortablejs'
  import {insertAfter} from '@/utils/dom'

  export default {
    name: 'XdhGrid',
    /**
     * 属性参数
     * @property {Boolean | Object} [sortable = false] sortable sort控件实例的配置项 https://segmentfault.com/a/1190000008209715
     * @property {Array} [list] list 传入的列表数据，不能双向绑定，list 和 value 只能设置一个
     * @property {Array} [value] value 传入的列表数据, 可双向绑定，list 和 value 只能设置一个
     * @property {Function} [beforeAdd] beforeAdd 判定能否（拖入）增加子元素的个数， 在sort实例里onAdd()事件中触发。参数：fromList 拖入当前数组的元素的原来数组；item：当前被操作的元素；oldIndex：被操作元素的原index；newIndex 被操作元素要放入新数组中的新index
     * @property {Object} [props = {...}] props grid组件的子元素flex样式属性配置项
     * @property {String} [props.order] order 对应flex子元素样式属性 order
     * @property {String} [props.basis] basis 对应flex子元素样式属性 flex-basis
     * @property {String} [props.grow] grow 对应flex子元素样式属性 flex-grow
     * @property {String} [props.shrink] shrink 对应flex子元素样式属性 flex-shrink
     * @property {String} [props.align] align 对应flex子元素样式属性 align-self
     * @property {String} [direction] direction 同flex父组件样式属性, 只接受'row', 'row-reverse', 'column', 'column-reverse'
     * @property {String} [wrap] wrap 同flex父组件样式属性, 只接受'nowrap', 'wrap', 'wrap-reverse'
     * @property {String} [justyfy] justyfy 同flex父组件样式属性, 只接受'flex-start', 'flex-end', 'center', 'space-between', 'space-around'
     * @property {String} [alignItems] alignItems 同flex父组件样式属性, 只接受'flex-start', 'flex-end', 'center', 'baseline', 'stretch'
     * @property {String} [alignContent] direction 同flex父组件样式属性, 只接受'flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'stretch'
     */
    props: {
      // sortablejs 实例化参数选项
      sortable: {
        type: [Boolean, Object],
        default: false
      },
      // 列表数据
      list: {
        type: Array
      },
      // 列表数据，双向绑定，list 和 value 只能设置一个
      value: {
        type: Array
      },
      // 在添加前回调，如返回false，即阻止添加
      beforeAdd: {
        type: Function
      },
      // 列表项目数据字段映射
      props: {
        type: Object,
        default() {
          return {
            // 定义item在容器中的排列顺序，数值越小，排列越靠前， 默认值0
            order: 'order',

            // 定义在分配多余空间之前，item占据的主轴空间，浏览器器根据这个属性，计算主轴是否有多余空间
            basis: 'basis',

            // 定义item的放大比例
            grow: 'grow',

            // 定义item的缩小比例
            shrink: 'shrink',

            //  允许单个item有与其他item不一样的对齐方式， 设置值与align-items 一样
            align: 'align'
          }
        }
      },
      // 决定主轴得分方向，即item的排列方向
      direction: {
        type: String,
        validator(value) {
          return ['row', 'row-reverse', 'column', 'column-reverse'].includes(value)
        }
      },
      // 确定容器内item是否可换行
      wrap: {
        type: String,
        validator(value) {
          return ['nowrap', 'wrap', 'wrap-reverse'].includes(value)
        }
      },
      // 定义item在主轴的对齐方式
      justify: {
        type: String,
        validator(value) {
          return ['flex-start', 'flex-end', 'center', 'space-between', 'space-around'].includes(value)
        }
      },
      // 定义item在交叉轴上的对其方式
      alignItems: {
        type: String,
        validator(value) {
          return ['flex-start', 'flex-end', 'center', 'baseline', 'stretch'].includes(value)
        }
      },
      // 定义多根轴线的对齐方式，如果items只有一根轴线，该设置将不起作用
      alignContent: {
        type: String,
        validator(value) {
          return ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'stretch'].includes(value)
        }
      }
    },
    data() {
      return {
        sortInstance: null,
        currentValue: null
      }
    },
    watch: {
      originalList(val) {
        this.$refs.container && (this.$refs.container.__list__ = val)
      }
    },
    computed: {
      // 复制传入的数组
      originalList() {
        return (this.list || this.value).slice(0)
      },
      // 从定义排序功能的配置项，组件内部监听排序控件的事件，并作为自己的排序事件派发
      sortOptions() {
        if (!this.sortable) {
          return null
        }
        return Object.assign({
          animation: 150
        }, this.sortable, {
          onUpdate: (e) => {
            this.sortValue(e.oldIndex, e.newIndex)
            this.$emit('on-update', e)
          },
          onAdd: (e) => {
            this.addValue(e.from.__list__, e.item, e.oldIndex, e.newIndex)
            this.$emit('on-add', e)
          },
          onRemove: (e) => {
            this.removeValue(e.item, e.oldIndex)
            this.$emit('on-remove', e)
          },
          onChoose: (e) => {
            this.$emit('on-choose', e)
          },
          onStart: (e) => {
            this.$emit('on-start', e)
          },
          onEnd: (e) => {
            this.$emit('on-end', e)
          },
          onSort: (e) => {
            this.$emit('on-sort', e)
          },
          onFilter: (e) => {
            this.$emit('on-filter', e)
          },
          onMove: (e) => {
            this.$emit('on-move', e)
          },
          onClone: (e) => {
            this.$emit('on-clone', e)
          }
        })
      },
      // 根据props 中传入的与 flex布局样式有关的属性 定义 组件使用的样式类
      classes() {
        return {
          [`xdh-grid--direction-${this.direction}`]: !!this.direction,
          [`xdh-grid--wrap-${this.wrap}`]: !!this.wrap,
          [`xdh-grid--justify-${this.justify}`]: !!this.justify,
          [`xdh-grid--align-items-${this.alignItems}`]: !!this.alignItems,
          [`xdh-grid--align-content-${this.alignContent}`]: !!this.alignContent,
          'xdh-grid--sortable': !!this.sortOptions
        }
      }
    },
    methods: {
      // 根据 props.prop属性(obj)的配置项，来定义grid内部子元素的flex属性相关样式
      styles(item) {
        /**
         * grid子元素flex样式设置函数, list遍历生成子元素时根据属性"props"配置项生成子节点的flex样式属性
         * @param {object} item
         */
        let style = {}
        if (item[this.props.order]) {
          style.order = item[this.props.order]
        }
        if (item[this.props.basis]) {
          style['flex-basis'] = item[this.props.basis]
        }
        if (item[this.props.grow]) {
          style['flex-grow'] = item[this.props.grow]
        }
        if (item[this.props.shrink]) {
          style['flex-shrink'] = item[this.props.shrink]
        }
        if (item[this.props.align]) {
          style['align-self'] = item[this.props.align]
        }
        return style
      },
      resetOrder(list) {
        /**
         * 重置list的顺序， 在“sortValue”函数中调用
         * @param {Array} list
         */
        let order = list.map((n, index) => {
          return index.toString() // 将当前(新)list的index转为字符串输出
        })
        this.sortInstance && this.sortInstance.sort(order) // 将生成的"order"传入sortable实例完成顺序重置
      },
      // 在当前grid组件内，删除对应的子元素
      removeItem(item) {
        Array.from(this.$refs.container.children).forEach(el => {
          if (el === item) {
            this.$refs.container.removeChild(el)
          }
        })
      },
      // 在当前grid组件内，将 item 插入index 位置中
      addItem(item, index) {
        let nodes = this.$refs.container.children
        if (nodes.length > 0) {
          if (index > 0) {
            insertAfter(item, nodes[index - 1])
          } else {
            this.$refs.container.insertBefore(item, nodes[0])
          }
        } else {
          this.$refs.container.appendChild(item)
        }
      },
      // 在排序控件改变数据（onUpdate）事件中触发，对传入的list作出修改，并且派发input事件实现数据双向绑定
      /**
       * 双向绑定改变Value的函数,在排序控件改变数据（onUpdate）事件中触发，对value的顺序进行改变
       * @function sortValue
       * @param oldIndex 排序前操作元素的index
       * @param newIndex 排序后操作元素的index
       */
      sortValue(oldIndex, newIndex) {
        if (!this.value) return
        let list = this.currentValue || this.originalList.slice(0)
        let oldTemp = list[oldIndex]
        list.splice(oldIndex, 1)
        list.splice(newIndex, 0, oldTemp)
        this.currentValue = list
        this.resetOrder(list)
        this.$emit('input', list)
      },
      /**
       * 双向绑定改变Value的函数,在排序控件改变数据（onAdd）事件中触发，当有其他数组的元素被拖入时，对绑定数组添加新元素
       * @function addValue
       * @param fromList 拖入元素的原数组
       * @param item  拖入操作的元素
       * @param oldIndex 拖入元素在原数组中的index
       * @param newIndex 拖入元素放在当前数组的index
       */
      addValue(fromList, item, oldIndex, newIndex) {
        if (!this.value) return
        if (this.beforeAdd && this.beforeAdd(fromList, item, oldIndex, newIndex) === false) {
          return
        }
        let list = this.currentValue || this.originalList.slice(0)
        let newItem = fromList[oldIndex]
        list.splice(newIndex, 0, newItem)
        this.currentValue = list
        this.removeItem(item)
        this.$emit('input', list.slice())
      },
      // 子元素在（可以被）拖动移出时，在拖动移出后，删除被拖走的元素， 在sortable实例的onRemove() 事件中调用
      /**
       * 双向绑定改变Value的函数,在排序控件改变数据（onRemove）事件中触发, 当列表元素可被拖走（非clone）且被拖走时，对绑定的数组删除元素
       * @function removeValue
       * @param item
       * @param oldIndex
       */
      removeValue(item, oldIndex) {
        if (!this.value) return
        if (this.sortOptions && this.sortOptions.group && this.sortOptions.group.pull === 'clone') {
          return
        }
        let list = this.currentValue || this.originalList.slice(0)
        list.splice(oldIndex, 1)
        this.currentValue = list
        this.addItem(item, oldIndex)
        this.$emit('input', list.slice())
      },
      // 创建排序控件实例
      initSortable() {
        this.sortInstance = new Sortable(this.$el, this.sortOptions)
      }
    },
    mounted() {
      // 组件创建后， 将container节点 设置自定义属性__list__ 指向 “this.originalList”（复制的新数组）
      this.$refs.container.__list__ = this.originalList
      // 初始化 sortable 实例
      this.sortOptions && this.initSortable()
    },
    beforeDestroy() {
      // 清除container 上的__list__数据指向
      this.$refs.container.__list__ = null
      // 销毁sortable实例
      this.sortInstance && this.sortInstance.destroy()
    }
  }
</script>
